This page provides detailed technical documentation for future students, including implementation details, design patterns, debugging techniques, and common pitfalls.

SPI Protocol Implementation

SPI Mode 0 Configuration

The system uses SPI Mode 0 (CPOL=0, CPHA=0) for all SPI interfaces:

  • CPOL=0: Clock idle state is LOW
  • CPHA=0: Data is sampled on the rising edge of SCK, changed on the falling edge
  • Bit Order: MSB first
  • Clock Speed: 100kHz for Arduino→FPGA, configurable for FPGA→MCU

FPGA SPI Slave Implementation

Key Implementation Details:

  1. CS-Based Protocol:

    • Transaction begins when CS goes LOW
    • Data is valid during CS LOW
    • Transaction ends when CS goes HIGH
    • CS must remain LOW for entire 16-byte packet transmission
    • For SPI Mode 0, CS HIGH guarantees SCK is idle (CPOL=0)
  2. Clock Edge Detection (from arduino_spi_slave.sv):

    // Shift in data on SCK rising edge (CPHA=0)
    always_ff @(posedge sck) begin
        if (cs_n_prev_sck && !cs_n) begin
            // CS falling edge - new transaction, reset shift register
            packet_shift <= 128'd0;
        end else if (!cs_n) begin
            // CS low - shift in data (MSB first)
            packet_shift <= {packet_shift[126:0], sdi};
        end
        // When CS is high, packet_shift retains value for CDC capture
    end
  3. Packet Framing:

    • First byte is header (0xAA) for synchronization
    • Fixed 16-byte packet size (128 bits total)
    • Header validation ensures packet integrity
    • Uses 128-bit shift register for efficient bit-level reception
  4. Clock Domain Crossing (CDC):

    • Data received in SCK domain (Arduino SCK, 100kHz)
    • Must be transferred to FPGA system clock domain (3MHz)
    • Strategy: CS-based safe read
      • Wait for CS HIGH (transaction complete, SCK guaranteed idle)
      • Wait 3 system clock cycles (1us) for settling
      • Atomic read of all 16 bytes in one clock cycle
      • Provides 10:1 timing margin (very safe)

MCU SPI Master Implementation

STM32L432KC SPI Configuration:

// SPI1 Configuration
// PB3 = SCK, PB5 = MOSI, PB4 = MISO, PA11 = NSS
SPI_InitTypeDef SPI_InitStruct;
SPI_InitStruct.Mode = SPI_MODE_MASTER;
SPI_InitStruct.Direction = SPI_DIRECTION_2LINES;
SPI_InitStruct.DataSize = SPI_DATASIZE_8BIT;
SPI_InitStruct.CLKPolarity = SPI_POLARITY_LOW;      // CPOL=0
SPI_InitStruct.CLKPhase = SPI_PHASE_1EDGE;          // CPHA=0
SPI_InitStruct.NSS = SPI_NSS_SOFT;                  // Software-controlled CS
SPI_InitStruct.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32; // Adjust for desired speed
SPI_InitStruct.FirstBit = SPI_FIRSTBIT_MSB;         // MSB first
SPI_InitStruct.TIMode = SPI_TIMODE_DISABLE;
SPI_InitStruct.CRCCalculation = SPI_CRCCALCULATION_DISABLE;

Key Considerations: - NSS (CS) must be manually controlled in software mode (PA11) - Ensure proper timing between CS assertion and data transfer - Handle SPI busy flag correctly before starting new transaction - Use proper delay between transactions to allow FPGA to update data - Read 16 bytes in sequence, parsing header (0xAA) and data fields - FPGA operates in read-only mode (ignores MOSI, only shifts out on MISO)

FPGA Design Patterns

Dual SPI Slave Architecture

Pattern: Implementing multiple SPI slave interfaces on a single FPGA

Implementation: - Separate state machines for each SPI slave - Independent clock domains (each SCK is asynchronous) - Separate data buffers for each interface - Clear data flow path between interfaces

Key Module Structure:

module drum_trigger_top(
    // Arduino SPI Interface
    input  logic arduino_sck,
    input  logic arduino_sdi,
    input  logic arduino_cs_n,
    
    // MCU SPI Interface
    input  logic mcu_sck,
    input  logic mcu_sdi,
    output logic mcu_sdo,
    input  logic mcu_cs_n,
    
    // Internal data path
    // Data flows: arduino_spi_slave → buffer → spi_slave_mcu
);

Clock Domain Crossing

Challenge: Handling asynchronous SPI clocks from different masters

Solution: - Use separate clock domains for each SPI interface - Synchronize control signals using synchronizers - Use valid/ready handshaking for data transfer - Avoid mixing clock domains in combinational logic

State Machine Design

Pattern: Clear state machine for SPI transaction handling

States: - IDLE: Waiting for transaction - RECEIVING: Actively receiving data - PROCESSING: Validating and processing packet - READY: Data available for next stage - ERROR: Error condition detected

Benefits: - Clear transaction lifecycle - Easy to debug - Predictable behavior - Error handling built-in

MCU Integration Techniques

SPI Master Configuration

Initialization Sequence: 1. Enable GPIO clocks 2. Configure SPI pins (alternate function) 3. Enable SPI peripheral clock 4. Configure SPI peripheral 5. Enable SPI peripheral

Key Code Pattern:

void SPI_Init(void) {
    // 1. Enable clocks
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOBEN;
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
    
    // 2. Configure GPIO pins
    GPIOB->MODER &= ~(GPIO_MODER_MODE3 | GPIO_MODER_MODE4 | GPIO_MODER_MODE5);
    GPIOB->MODER |= (GPIO_MODER_MODE3_1 | GPIO_MODER_MODE4_1 | GPIO_MODER_MODE5_1);
    GPIOB->AFR[0] |= (5 << 12) | (5 << 16) | (5 << 20); // AF5 for SPI1
    
    // 3. Configure SPI
    // ... SPI configuration code ...
    
    // 4. Enable SPI
    SPI1->CR1 |= SPI_CR1_SPE;
}

Reading Data from FPGA

Transaction Pattern:

uint8_t SPI_ReadByte(void) {
    // Wait for TX buffer empty
    while (!(SPI1->SR & SPI_SR_TXE));
    
    // Write dummy byte to generate clock
    SPI1->DR = 0xFF;
    
    // Wait for RX buffer full
    while (!(SPI1->SR & SPI_SR_RXNE));
    
    // Read received byte
    return SPI1->DR;
}

Data Processing

Packet Parsing (from DATA_PIPELINE_VERIFICATION.md): - Header validation: Byte 0 must be 0xAA - Roll: Bytes 1-2 (int16_t, MSB first, scaled by 100 = 0.01 degree resolution) - Pitch: Bytes 3-4 (int16_t, MSB first, scaled by 100) - Yaw: Bytes 5-6 (int16_t, MSB first, scaled by 100) - Gyro X: Bytes 7-8 (int16_t, MSB first, scaled by 2000) - Gyro Y: Bytes 9-10 (int16_t, MSB first, scaled by 2000) - Gyro Z: Bytes 11-12 (int16_t, MSB first, scaled by 2000) - Flags: Byte 13 (bit 0 = Euler valid, bit 1 = Gyro valid) - Reserved: Bytes 14-15 (0x00)

Data Handling: - All values are signed 16-bit integers (int16_t) - Euler angles: Divide by 100 to get degrees - Gyroscope: Divide by 2000 to get actual units (check sensor datasheet for exact scaling) - Check valid flags before using data - Handle endianness correctly (MSB first, big-endian)

Sensor Interface Methods

BNO085 I2C Interface (Arduino Side)

Key Implementation (from ARDUINO_SENSOR_BRIDGE.ino): - Uses Adafruit BNO08x library - I2C communication at 400kHz - Sensor reports at 100Hz (10ms intervals, 10000 microseconds) - Quaternion and gyroscope data from sensor

Configuration:

Adafruit_BNO08x bno08x(BNO08X_RESET);
sh2_SensorValue_t sensorValue;

// Enable rotation vector report (quaternion)
sh2_SensorId_t reportType = SH2_ROTATION_VECTOR;
sh2_ReportInterval_t interval = 10000; // 100Hz (10000 microseconds)
bno08x.enableReport(reportType, interval);

// Enable gyroscope report
reportType = SH2_GYROSCOPE_CALIBRATED;
bno08x.enableReport(reportType, interval);

Data Conversion

Quaternion to Euler: - Convert quaternion (w, x, y, z) to Euler angles (roll, pitch, yaw) - Scale by 100 for transmission (0.01 degree resolution) - Handle angle wrapping correctly - Store as signed 16-bit integers

Packet Formatting (16 bytes total): - Byte 0: Header (0xAA) for synchronization - Bytes 1-2: Roll (int16_t, MSB first, scaled by 100) - Bytes 3-4: Pitch (int16_t, MSB first, scaled by 100) - Bytes 5-6: Yaw (int16_t, MSB first, scaled by 100) - Bytes 7-8: Gyro X (int16_t, MSB first, scaled by 2000) - Bytes 9-10: Gyro Y (int16_t, MSB first, scaled by 2000) - Bytes 11-12: Gyro Z (int16_t, MSB first, scaled by 2000) - Byte 13: Flags (bit 0 = Euler valid, bit 1 = Gyro valid) - Bytes 14-15: Reserved (0x00)

SPI Transmission: - CS pin: D10 (FPGA_SPI_CS) - connects to FPGA arduino_cs_n - Clock: D13 (SCK) - Data: D11 (MOSI) - Mode: SPI_MODE0, MSBFIRST, 100kHz

Debugging Approaches

FPGA Debugging

1. Simulation: - Use testbenches for each module - Verify SPI protocol compliance - Check timing constraints - Validate packet parsing

2. Signal Tap / Logic Analyzer: - Monitor SPI signals (SCK, MOSI, MISO, CS) - Verify timing relationships - Check data values - Debug state machine transitions

3. Status LEDs: - Implement LEDs for key states: - Initialized - Data valid - Error condition - Heartbeat

MCU Debugging

1. Serial Output: - Use USART for debug messages - Print received data values - Log SPI transaction status - Monitor timing

2. Breakpoints: - Use debugger to step through code - Inspect register values - Check SPI peripheral status - Verify data buffers

3. Oscilloscope: - Monitor SPI signals - Verify timing - Check CS timing - Validate data transmission

System-Level Debugging

1. End-to-End Testing: - Start with known good sensor data - Verify each stage of pipeline - Check data at each interface - Validate final output

2. Incremental Integration: - Test Arduino → FPGA first - Then test FPGA → MCU - Finally test complete system - Isolate problems to specific stage

Common Pitfalls and Solutions

Pitfall 1: SPI Clock Domain Crossing (CDC) Issues

Problem: Metastability or timing violations when transferring data between asynchronous clock domains (Arduino SCK → FPGA clk → MCU SCK)

Solution (from SENIOR_ENGINEER_AUDIT.md): - Use CS-based safe read approach: wait for CS HIGH (transaction complete) - For SPI Mode 0, CS HIGH guarantees SCK is idle (CPOL=0) - Wait 3 system clock cycles after CS HIGH before reading (provides 10:1 timing margin) - Use atomic reads of complete packets (all 16 bytes in one cycle) - Document timing constraints and CDC strategy - See TIMING_CONSTRAINTS.md for complete analysis

Pitfall 2: CS Timing Errors

Problem: CS asserted/deasserted at wrong times, causing data corruption

Solution: - Ensure CS stays LOW for entire packet - Don’t change CS during active transaction - Add proper delays between transactions - Verify CS timing with oscilloscope

Pitfall 3: Data Format Mismatches

Problem: Data format doesn’t match between stages

Solution: - Document data formats clearly - Use consistent scaling factors - Verify endianness (MSB/LSB order) - Test with known values

Pitfall 4: Buffer Overflow

Problem: New data arrives before old data is read

Solution: - Implement proper buffering - Use valid flags to indicate data availability - Check buffer status before writing - Handle overflow conditions gracefully

Pitfall 5: Power and Ground Issues

Problem: System instability due to power/ground problems

Solution: - Use common ground for all components - Ensure adequate power supply capacity - Add decoupling capacitors - Verify power supply voltage levels

Pitfall 6: SPI Mode Mismatch

Problem: Different SPI modes between components

Solution: - Verify all components use same SPI mode - Check CPOL and CPHA settings - Ensure bit order matches (MSB/LSB) - Verify clock polarity and phase

Additional Resources

Documentation Files

The code repository includes several comprehensive documentation files:

  • DATA_PIPELINE_VERIFICATION.md: Complete data flow verification showing exact byte-by-byte packet format through all stages (Arduino → FPGA → MCU)
  • ARDUINO_FPGA_CONNECTION_ISSUE.md: Troubleshooting guide for Arduino-FPGA connection issues, CS pin configuration, and SPI settings
  • TIMING_CONSTRAINTS.md: Complete FPGA timing analysis, clock domain analysis, CDC strategies, and recommended timing constraints for synthesis tools
  • SENIOR_ENGINEER_AUDIT.md: Comprehensive code review addressing CDC violations, timing issues, and implementation improvements (all critical issues resolved)

Key Learnings

  1. SPI Protocol: Understanding SPI modes and timing is critical
  2. Clock Domains: Proper handling of asynchronous clocks prevents many issues
  3. Data Formats: Consistent data formats across all stages simplifies debugging
  4. Incremental Testing: Test each stage independently before system integration
  5. Documentation: Good documentation saves time during debugging

Future Enhancements

Potential improvements for future iterations: - Error detection and correction - Data rate adaptation - Additional sensor support - Real-time filtering - Wireless communication